package com.matinzac.service.impl;

import cn.hutool.core.io.FileUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.matinzac.config.WxPayConfig;
import com.matinzac.constants.WxPayConstants;
import com.matinzac.entity.PayOrder;
import com.matinzac.service.WxPayService;
import com.matinzac.util.*;
import com.matinzac.util.UUID;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.cert.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.*;

/**
 * <p>Description:微信支付具体实现</p>
 *
 * @author MtinZac
 * create on  2021/11/26 上午10:40
 */
@Slf4j
@Service
public class WxPayServiceImpl implements WxPayService {

    @Resource
    private WxPayConfig wxPayConfig;


    @Override
    public String getMchSerialNo() {
        //获取证书编号
        X509Certificate certificate = getCertificate(FileUtil.getInputStream(wxPayConfig.getCertPath()));
        String serialNo = certificate.getSerialNumber().toString(16).toUpperCase();
        log.info("获取的证书编号为:{}",serialNo);
        return serialNo;
    }

    @Override
    public String getOpenId(String code) {
        log.info("获取code:{}", code);
        String url = WxPayConstants.COUDE_OPENID_URL
                + wxPayConfig.getAppId() + "&secret=" + wxPayConfig.getSecret() + "&js_code=" + code + "&grant_type=authorization_code";
        String res = HttpUtil.get(url);
        System.out.println(res);
        JSONObject object = JSONObject.parseObject(res);
        String openid = object.getString("openid");
        String unionid = object.getString("unionid");
        log.info("根据code换取openId:{}", openid);

        return openid;
    }

    @Override
    public Map<String, Object> getPayInfo(String openid, String time_expire, PayOrder order) {
        Map<String, Object> map = new HashMap();
        // 支付的产品（小程序或者公众号，主要需要和微信支付绑定哦）
        map.put("appid", wxPayConfig.getAppId());
        // 支付的商户号
        map.put("mchid", wxPayConfig.getMchId());
        //临时写死配置
        map.put("description", order.getRemarkBody());
        map.put("out_trade_no", order.getOutTradeNo());
        map.put("notify_url", wxPayConfig.getNotifyOrderUrl());

        //判断过期时间是否为null 有过期时间则进行添加
        if (!StringUtils.isEmpty(time_expire)) {
            map.put("time_expire", time_expire);
        }

        Map<String, Object> amount = new HashMap();
        //订单金额 单位分,需要* 100转换为元
        amount.put("total", Integer.parseInt(order.getTotalFee()) * 100);
        amount.put("currency", "CNY");
        map.put("amount", amount);
        // 设置小程序所需的opendi
        Map<String, Object> payermap = new HashMap();
        payermap.put("openid", openid);
        map.put("payer", payermap);

        ObjectMapper objectMapper = new ObjectMapper();
        String body = null;
        try {
            body = objectMapper.writeValueAsString(map);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }

        Map<String, Object> stringObjectMap = null;
        HashMap<String, Object> dataMap = null;
        try {
            //获取签名token
            String token = getToken("POST", new URL(WxPayConstants.JSAPIURL), body);
            //发送支付请求
            stringObjectMap = HttpUtils.doPostWexin(WxPayConstants.JSAPIURL, body, token);
            log.info("获取支付凭证信息为:" + stringObjectMap);
            dataMap = getTokenJSAPI(wxPayConfig.getAppId(), String.valueOf(stringObjectMap.get("prepay_id")));
            log.info("返回支付参数为:" + dataMap);
            return dataMap;
        } catch (Exception ex) {
            log.info("返回支付信息体异常");
        }
        return null;
    }

    @Override
    public Map<String, Object> callBack(Map body, HttpServletRequest request) {
        log.info(DateUtils.getTime() + "微信支付回调开始");
        Map<String, Object> result = new HashMap();
        //1：获取微信支付回调的获取签名信息
        String timestamp = request.getHeader("Wechatpay-Timestamp");
        String nonce = request.getHeader("Wechatpay-Nonce");
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            // 2: 开始解析报文体
            String data = objectMapper.writeValueAsString(body);
            String message = timestamp + "\n" + nonce + "\n" + data + "\n";
            //3：获取应答签名
            String sign = request.getHeader("Wechatpay-Signature");
            //4：获取平台对应的证书
            String serialNo = request.getHeader("Wechatpay-Serial");
            if (!wxPayConfig.certificateMap.containsKey(serialNo)) {
                //获取验证签名的token 校验证书
                String token = getToken("GET", new URL(WxPayConstants.CERTIFICATESURL), "");
                wxPayConfig.certificateMap = refreshCertificate(token);
            }

            //获取证书编号验证签名
            X509Certificate x509Certificate = wxPayConfig.certificateMap.get(serialNo);
            if (!verify(x509Certificate, message.getBytes(), sign)) {
                throw new IllegalArgumentException("微信支付签名验证失败:" + message);
            }
            //    log.info("签名验证成功");
            Map<String, String> resource = (Map) body.get("resource");
            // 5：回调报文解密
            AesUtil aesUtil = new AesUtil(wxPayConfig.getV3Key().getBytes());
            //解密后json字符串
            String decryptToString = aesUtil.decryptToString(
                    resource.get("associated_data").getBytes(),
                    resource.get("nonce").getBytes(),
                    resource.get("ciphertext"));
            log.info("返回字符串解密前为------------->decryptToString====>{}", decryptToString);

            //6：获取微信支付返回的信息
            Map<String, Object> jsonData = objectMapper.readValue(decryptToString, Map.class);
            log.info(DateUtils.getTime() + "微信回调携带数据解密后为:");
            //7: 支付状态的判断 如果是success就代表支付成功
            // 8：获取支付的交易单号，流水号，和附属参数
            String out_trade_no = jsonData.get("out_trade_no").toString();
            log.info("付款的订单号为:" + out_trade_no);
            if ("SUCCESS".equals(jsonData.get("trade_state"))) {
                //流水号
                String transaction_id = jsonData.get("transaction_id").toString();
                String success_time = jsonData.get("success_time").toString();
                String attach = jsonData.get("attach").toString();
                //根据订单号查询支付状态，如果未支付，更新支付状态 为已支付
                // TODO 更新自己的订单状态为已支付,将支付信息进行存储(保存支付信息)


            } else {
                //支付失败或取消情况下
                // TODO 支付失败或者其他状态修改订单状态或者根据微信的错误状态码进行路傲娇处理
            }
            result.put("code", "SUCCESS");
            result.put("message", "成功");
        } catch (Exception e) {
            result.put("code", "fail");
            result.put("message", "系统错误");
            e.printStackTrace();
        }
        return result;
    }


    /**
     * 参考网站 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_4.shtml
     * 计算签名值
     *
     * @param appId     根据openId
     * @param prepay_id 预支付交易会话标识(通过携带支付参数调用JSAPI下单接口)返回交易标识符
     * @return
     * @throws IOException
     * @throws SignatureException
     * @throws NoSuchAlgorithmException
     * @throws
     */
    public HashMap<String, Object> getTokenJSAPI(String appId, String prepay_id) throws IOException, SignatureException, NoSuchAlgorithmException, InvalidKeyException {
        // 获取随机字符串
        String nonceStr = getNonceStr();
        // 获取微信小程序支付package
        String packagestr = "prepay_id=" + prepay_id;
        long timestamp = System.currentTimeMillis() / 1000;
        //签名，使用字段appId、timeStamp、nonceStr、package计算得出的签名值
        String message = buildMessageTwo(appId, timestamp, nonceStr, packagestr);
        //获取对应的签名
        String signature = sign(message.getBytes("utf-8"));
        // 组装返回
        HashMap<String, Object> map = new HashMap<>();
        map.put("appId", appId);
        map.put("timeStamp", String.valueOf(timestamp));
        map.put("nonceStr", nonceStr);
        map.put("package", packagestr);
        map.put("signType", "RSA");
        map.put("paySign", signature);
        return map;
    }


    /**
     * 获取私钥
     *
     * @param filename 私钥文件路径  (required)
     * @return 私钥对象
     */
    public static PrivateKey getPrivateKey(String filename) throws IOException {
        System.out.println("filename:" + filename);
        String content = new String(Files.readAllBytes(Paths.get(filename)), "utf-8");
        try {
            String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
                    .replace("-----END PRIVATE KEY-----", "")
                    .replaceAll("\\s+", "");

            KeyFactory kf = KeyFactory.getInstance("RSA");
            return kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持RSA", e);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException("无效的密钥格式");
        }
    }

    /**
     * 生成token 也就是生成签名
     *
     * @param method 请求类型( GET, POST)
     * @param url    获取token的URL 分别为下单 :https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi  查询证书 :https://api.mch.weixin.qq.com/v3/certificates
     * @param body   封装的支付凭证信息包括商户ID,APPID和证书序列号
     * @return
     * @throws Exception
     */
    public String getToken(String method, URL url, String body) throws Exception {
        String nonceStr = UUID.getNonceStr();
        long timestamp = System.currentTimeMillis() / 1000;
        String message = buildMessage(method, url, timestamp, nonceStr, body);
        String signature = sign(message.getBytes("utf-8"));

        return "WECHATPAY2-SHA256-RSA2048 " + "mchid=\"" + wxPayConfig.getMchId() + "\","
                + "nonce_str=\"" + nonceStr + "\","
                + "timestamp=\"" + timestamp + "\","
                + "serial_no=\"" + wxPayConfig.getMchSerialNo() + "\","
                + "signature=\"" + signature + "\"";
    }


    /**
     * 通过携带token获取平台证书
     *
     * @param token 获取证书的token https://api.mch.weixin.qq.com/v3/certificates
     * @return
     * @throws Exception
     */
    public Map<String, X509Certificate> refreshCertificate(String token) throws Exception {
        Map<String, X509Certificate> certificateMap = new HashMap();
        // 1: 执行get请求
        JsonNode jsonNode = HttpUtils.doGet(WxPayConstants.CERTIFICATESURL, token);
        // 2: 获取平台验证的相关参数信息
        JsonNode data = jsonNode.get("data");
        if (data != null) {
            for (int i = 0; i < data.size(); i++) {
                JsonNode encrypt_certificate = data.get(i).get("encrypt_certificate");
                //对关键信息进行解密
                AesUtil aesUtil = new AesUtil(wxPayConfig.getV3Key().getBytes());
                String associated_data = encrypt_certificate.get("associated_data").toString().replaceAll("\"", "");
                String nonce = encrypt_certificate.get("nonce").toString().replaceAll("\"", "");
                String ciphertext = encrypt_certificate.get("ciphertext").toString().replaceAll("\"", "");
                //证书内容
                String certStr = aesUtil.decryptToString(associated_data.getBytes(), nonce.getBytes(), ciphertext);
                //证书内容转成证书对象
                CertificateFactory cf = CertificateFactory.getInstance("X509");
                X509Certificate x509Cert = (X509Certificate) cf.generateCertificate(
                        new ByteArrayInputStream(certStr.getBytes("utf-8"))
                );
                String serial_no = data.get(i).get("serial_no").toString().replaceAll("\"", "");
                certificateMap.put(serial_no, x509Cert);
            }
        }
        return certificateMap;
    }


    /**
     * 生成签名信息
     *
     * @param method    请求类型( GET, POST)
     * @param url       请求的URL
     * @param timestamp 时间戳
     * @param nonceStr  32 位的随机字符串
     * @param body      封装的支付凭证信息包括商户ID,APPID和证书序列号
     * @return
     */
    public static String buildMessage(String method, URL url, long timestamp, String nonceStr, String body) {
        String canonicalUrl = url.getPath();
        if (url.getQuery() != null) {
            canonicalUrl += "?" + url.getQuery();
        }
        return method + "\n"
                + canonicalUrl + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + body + "\n";
    }

    /**
     * 生成 32 位随机字符串
     *
     * @return 32 位的随机字符串
     */
    public static String getNonceStr() {
        return UUID.randomUUID().toString()
                .replaceAll("-", "")
                .substring(0, 32);
    }

    /**
     * 验证签名
     *
     * @param certificate
     * @param message
     * @param signature
     * @return
     */
    public static boolean verify(X509Certificate certificate, byte[] message, String signature) {
        try {
            Signature sign = Signature.getInstance("SHA256withRSA");
            sign.initVerify(certificate);
            sign.update(message);
            return sign.verify(Base64.getDecoder().decode(signature));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
        } catch (SignatureException e) {
            throw new RuntimeException("签名验证过程发生了错误", e);
        } catch (InvalidKeyException e) {
            throw new RuntimeException("无效的证书", e);
        }
    }


    /**
     * 拼接请求参数
     *
     * @param appId     小程序 id
     * @param timestamp 时间戳
     * @param nonceStr  通知随机字符串
     * @param packag
     * @return
     */
    private static String buildMessageTwo(String appId, long timestamp, String nonceStr, String packag) {
        return appId + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + packag + "\n";
    }


    /**
     * 拿到私钥文件生成签名
     *
     * @param message
     * @return
     * @throws NoSuchAlgorithmException
     * @throws SignatureException
     * @throws IOException
     * @throws InvalidKeyException
     */
    private String sign(byte[] message) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException {
        Signature sign = Signature.getInstance("SHA256withRSA"); //SHA256withRSA
        sign.initSign(getPrivateKey(wxPayConfig.getKeyPath()));
        sign.update(message);
        return Base64.getEncoder().encodeToString(sign.sign());
    }


    /**
     * 通过cert 文件流获取证书编号
     * @param inputStream 文件输入流
     * @return 编码的证书编号
     */
    public static X509Certificate getCertificate(InputStream inputStream) {
        try {
            CertificateFactory cf = CertificateFactory.getInstance("X509");
            X509Certificate cert = (X509Certificate)cf.generateCertificate(inputStream);
            cert.checkValidity();
            return cert;
        } catch (CertificateExpiredException var3) {
            throw new RuntimeException("证书已过期", var3);
        } catch (CertificateNotYetValidException var4) {
            throw new RuntimeException("证书尚未生效", var4);
        } catch (CertificateException var5) {
            throw new RuntimeException("无效的证书", var5);
        }
    }


}
